package edu.upenn.cis.cis555.webserver;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Locale;
import java.util.Vector;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.Cookie;

public class HttpServletResponse implements javax.servlet.http.HttpServletResponse {
	private PrintWriter m_writer;
	private PrintWriter header_buff;
	private HashMap<String,StringBuffer> respHeaders;
	private boolean isCommitted;
	private int status_code;
	private int content_length;
	private String content_type;
	private Locale theLocale;
	private String char_encoding;
	private int m_httpVersion;
	private DataOutputStream m_output;
	private int buffer_size;
	private Vector<Cookie> m_cookies;
	
	public HashMap<String,StringBuffer> getRHS() {
		return respHeaders;
	}
	
	public HttpServletResponse(DataOutputStream output, int httpVersion, Vector<Cookie> cookies) {
		header_buff = new PrintWriter(output);
		respHeaders = new HashMap<String,StringBuffer>();
		isCommitted = false;
		status_code = 200;
		content_type = "text/html";
		content_length = 0;
		m_httpVersion = httpVersion;
		m_output = output;
		buffer_size = 0;
		m_cookies = cookies;
	}

	@Override //DONE
	public void addCookie(Cookie arg0) {
		// Adds the specified cookie to the response. This method can be called multiple times to set more than one cookie.
		for (Cookie ac : m_cookies)
			if (ac.equals(arg0)) {
				ac.setMaxAge(arg0.getMaxAge());
				return;
			}
		m_cookies.add(arg0);
	}
	
	// Multiple-Value headers comma separated
//	It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing 
//	the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. The order 
//	in which header fields with the same field-name are received is therefore significant to the interpretation of the combined 
//	field value, and thus a proxy MUST NOT change the order of these field values when a message is forwarded.

	@Override //DONE
	public void addDateHeader(String name, long date) {
//	    Adds a response header with the given name and date-value. The date is specified in terms of milliseconds since the epoch. 
//	    This method allows response headers to have multiple values.
//
//	    Parameters:
//	        name - the name of the header to set
//	        date - the additional date value
		SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM HH:mm:ss zzz yyyy");
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(format.format(date)));
		}
		else {
			respHeaders.get(name).append(","+format.format(date));
		}
	}

	@Override //DONE
	public void addHeader(String name, String value) {
//	    Adds a response header with the given name and value. This method allows response headers to have multiple values.
//
//	    Parameters:
//	        name - the name of the header
//	        value - the additional header value If it contains octet string, it should be encoded according to RFC 2047 
//	        (http://www.ietf.org/rfc/rfc2047.txt)
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(value));
		}
		else {
			respHeaders.get(name).append(","+value);
		}
	}

	@Override //DONE
	public void addIntHeader(String name, int value) {
//	    Adds a response header with the given name and integer value. This method allows response headers to have multiple values.
//
//	    Parameters:
//	        name - the name of the header
//	        value - the assigned integer value
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(Integer.toString(value)));
		}
		else {
			respHeaders.get(name).append(","+Integer.toString(value));
		}
	}

	@Override //DONE
	public boolean containsHeader(String name) {
//	    Returns a boolean indicating whether the named response header has already been set.
//
//	    Parameters:
//	        name - the header name 
//	    Returns:
//	        true if the named response header has already been set; false otherwise
		if (respHeaders.keySet().size() == 0)
			return false;
		if (respHeaders.containsKey(name))
			return true;
		else
			return false;
			
	}

	@Override //DONE
	public String encodeRedirectURL(String arg0) {
//	    Encodes the specified URL for use in the sendRedirect method or, if encoding is not needed, returns the URL unchanged. 
//	    The implementation of this method includes the logic to determine whether the session ID needs to be encoded in the URL. 
//	    Because the rules for making this determination can differ from those used to decide whether to encode a normal link, 
//	    this method is separated from the encodeURL method.
//
//	    All URLs sent to the HttpServletResponse.sendRedirect method should be run through this method. Otherwise, URL rewriting 
//	    cannot be used with browsers which do not support cookies.
//
//	    Parameters:
//	        url - the url to be encoded. 
//	    Returns:
//	        the encoded URL if encoding is needed; the unchanged URL otherwise.
		return arg0;
	}

	@Override //DONE
	public String encodeURL(String arg0) {
//	    Encodes the specified URL by including the session ID in it, or, if encoding is not needed, returns the URL unchanged. 
//	    The implementation of this method includes the logic to determine whether the session ID needs to be encoded in the URL. 
//	    For example, if the browser supports cookies, or session tracking is turned off, URL encoding is unnecessary.
//
//	    For robust session tracking, all URLs emitted by a servlet should be run through this method. Otherwise, URL rewriting 
//	    cannot be used with browsers which do not support cookies.
//
//	    Parameters:
//	        url - the url to be encoded. 
//	    Returns:
//	        the encoded URL if encoding is needed; the unchanged URL otherwise.
		return arg0;
	}

	@Override //DONE
	public void sendError(int arg0) throws IOException {
//	    Sends an error response to the client using the specified status code and clearing the buffer.
//
//	    If the response has already been committed, this method throws an IllegalStateException. After using this method, the response 
//	    should be considered to be committed and should not be written to.
//
//	    Parameters:
//	        sc - the error status code 
//	    Throws:
//	        java.io.IOException - If an input or output exception occurs 
//	        java.lang.IllegalStateException - If the response was committed before this method call
		if (isCommitted()) {
			throw new IllegalStateException();
		}
		else {
			status_code = arg0;
			header_buff = new PrintWriter(m_output);
			header_buff.println("HTTP/1."+m_httpVersion+" "+status_code+" Error");
			respHeaders = new HashMap<String,StringBuffer>();
			flushBuffer();
		}
	}

	@Override //DONE
	public void sendError(int sc, String msg) throws IOException {
//	    Sends an error response to the client using the specified status. The server defaults to creating the response to look like an 
//	    HTML-formatted server error page containing the specified message, setting the content type to "text/html", leaving cookies and 
//	    other headers unmodified. If an error-page declaration has been made for the web application corresponding to the status code 
//	    passed in, it will be served back in preference to the suggested msg parameter.
//
//	    If the response has already been committed, this method throws an IllegalStateException. After using this method, the response 
//	    should be considered to be committed and should not be written to.
//
//	    Parameters:
//	        sc - the error status code
//	        msg - the descriptive message 
//	    Throws:
//	        java.io.IOException - If an input or output exception occurs 
//	        java.lang.IllegalStateException - If the response was committed
		if (isCommitted()) {
			throw new IllegalStateException();
		}
		else {
			status_code = sc;
			header_buff = new PrintWriter(m_output);
			header_buff.println("HTTP/1."+m_httpVersion+" "+status_code+" "+msg);
			PrintWriter out = getWriter();
			out.println("<HTML><HEAD><TITLE>"+sc+" Error</TITLE></HEAD><BODY>");
			out.println("<P>"+msg+"</P>");
			out.println("</BODY></HTML>");
			respHeaders = new HashMap<String,StringBuffer>();
			flushBuffer();
		}
	}

	@Override //DONE
	public void sendRedirect(String arg0) throws IOException {
//	    Sends a temporary redirect response to the client using the specified redirect location URL. This method can accept relative URLs; 
//	    the servlet container must convert the relative URL to an absolute URL before sending the response to the client. If the location 
//	    is relative without a leading '/' the container interprets it as relative to the current request URI. If the location is relative 
//	    with a leading '/' the container interprets it as relative to the servlet container root.
//
//	    If the response has already been committed, this method throws an IllegalStateException. After using this method, the response 
//	    should be considered to be committed and should not be written to.
//
//	    Parameters:
//	        location - the redirect location URL 
//	    Throws:
//	        java.io.IOException - If an input or output exception occurs 
//	        java.lang.IllegalStateException - If the response was committed or if a partial URL is given and cannot be converted into a 
//	        valid URL
		
		// Send a 302 Redirect <-- from assignment
		if (isCommitted()) {
			throw new IllegalStateException();
		}
		else {
			status_code = 302;
			header_buff = new PrintWriter(m_output);
			header_buff.println("HTTP/1."+m_httpVersion+" "+status_code+" Redirect");
			PrintWriter out = getWriter();
			out.println("<HTML><HEAD><TITLE>302 Redirect</TITLE></HEAD><BODY>");
			out.println("<P>302 Redirect</P>");
			out.println("</BODY></HTML>");
			respHeaders = new HashMap<String,StringBuffer>();
			flushBuffer();
		}
	}

	@Override //DONE
	public void setDateHeader(String name, long date) {
//	    Sets a response header with the given name and date-value. The date is specified in terms of milliseconds since the epoch. 
//	    If the header had already been set, the new value overwrites the previous one. The containsHeader method can be used to test 
//	    for the presence of a header before setting its value.
//
//	    Parameters:
//	        name - the name of the header to set
//	        date - the assigned date value
		SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz");
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(format.format(date)));
		}
		else {
			respHeaders.get(name).replace(0, respHeaders.get(name).length(), format.format(date));
		}
	}

	@Override //DONE
	public void setHeader(String name, String value) {
//	    Sets a response header with the given name and value. If the header had already been set, the new value overwrites the previous 
//	    one. The containsHeader method can be used to test for the presence of a header before setting its value.
//
//	    Parameters:
//	        name - the name of the header
//	        value - the header value If it contains octet string, it should be encoded according to RFC 2047 
//	        (http://www.ietf.org/rfc/rfc2047.txt)
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(value));
		}
		else {
			respHeaders.get(name).replace(0, respHeaders.get(name).length(), value);
		}
	}

	@Override //DONE
	public void setIntHeader(String name, int value) {
//	    Sets a response header with the given name and integer value. If the header had already been set, the new value overwrites 
//	    the previous one. The containsHeader method can be used to test for the presence of a header before setting its value.
//
//	    Parameters:
//	        name - the name of the header
//	        value - the assigned integer value
		if (!respHeaders.containsKey(name)) {
			respHeaders.put(name, new StringBuffer(Integer.toString(value)));
		}
		else {
			respHeaders.get(name).replace(0, respHeaders.get(name).length(), Integer.toString(value));
		}
	}

	@Override //DONE
	public void setStatus(int arg0) {
//	    Sets the status code for this response. This method is used to set the return status code when there is no error 
//	    (for example, for the status codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, and the caller wishes to invoke 
//	    an error-page defined in the web application, the sendError method should be used instead.
//
//	    The container clears the buffer and sets the Location header, preserving cookies and other headers.
//
//	    Parameters:
//	        sc - the status code
		status_code = arg0;
		String message = new String();
		switch (status_code) {
		case 200:
			message = "OK";
			break;
		case 301:
			message = "Moved Permanently";
			break;
		case 302:
			message = "Moved Temporarily";
			break;
		case 307:
			message = "Temporarily Redirect";
			break;
		case 504:
			message = "Gateway Timeout";
			break;
		}
		header_buff = new PrintWriter(m_output);
		header_buff.println("HTTP/1."+m_httpVersion+" "+status_code+" "+message);
	}

	@Override //DONE
	public void flushBuffer() throws IOException {
//	    Forces any content in the buffer to be written to the client. A call to this method automatically commits the response, 
//	    meaning the status code and headers will be written.
//
//	    Throws:
//	        java.io.IOException
		setStatus(status_code); //Default 200
		if (!respHeaders.keySet().isEmpty()) {
			if (!respHeaders.keySet().contains("date")) {
				setDateHeader("Date", System.currentTimeMillis());
			}
		}
		if (!respHeaders.keySet().isEmpty()) {
			for (String key : respHeaders.keySet()) {
				header_buff.println(key+": "+respHeaders.get(key));
			}
		}
		for (Cookie acookie : m_cookies) {
			header_buff.println("Set-Cookie: "+acookie.getName()+"="+acookie.getValue()+"; Max-Age="+acookie.getMaxAge()+"; Version="+acookie.getVersion());
		}
		for (HttpSession asession : HttpServer.sessions) {
			if (asession.isValid())
				header_buff.println("Set-Cookie: JSESSIONID="+asession.getId());
		}
			
		header_buff.println();
		header_buff.flush();
		m_writer.flush();
		isCommitted = true;
	}

	@Override //DONE
	public String getCharacterEncoding() {
		// Should return "ISO-8859-1".
		return "ISO-8859-1";
	}

	@Override //DONE
	public String getContentType() {
		// Should return "text/html" by default, and the results of setContentType if it was previously called.
		return content_type;
	}

	@Override //DONE
	public Locale getLocale() {
//		Returns the locale specified for this response using the setLocale(java.util.Locale) method. Calls made to setLocale after the 
//		response is committed have no effect. If no locale has been specified, the container's default locale is returned. 
		return theLocale;
	}

	@Override //DONE
	public PrintWriter getWriter() throws IOException {
//	    Returns a PrintWriter object that can send character text to the client. The PrintWriter uses the character encoding 
//	    returned by getCharacterEncoding(). If the response's character encoding has not been specified as described in 
//	    getCharacterEncoding (i.e., the method just returns the default value ISO-8859-1), getWriter updates it to ISO-8859-1.
//
//	    Calling flush() on the PrintWriter commits the response.
//
//	    Either this method or getOutputStream() may be called to write the body, not both.
//
//	    Returns:
//	        a PrintWriter object that can return character data to the client 
//	    Throws:
//	        UnsupportedEncodingException - if the character encoding returned by getCharacterEncoding cannot be used 
//	        java.lang.IllegalStateException - if the getOutputStream method has already been called for this response object 
//	        java.io.IOException - if an input or output exception occurred
		m_writer = new PrintWriter(new OutputStreamWriter(m_output, getCharacterEncoding()));
		return m_writer;
	}

	@Override //DONE
	public boolean isCommitted() {
//		Returns a boolean indicating if the response has been committed. A committed response has already had its status code 
//		and headers written.
		return isCommitted;
	}

	@Override //DONE
	public void reset() {
//	    Clears any data that exists in the buffer as well as the status code and headers. If the response has been committed, 
//	    this method throws an IllegalStateException.
//
//	    Throws:
//	        java.lang.IllegalStateException - if the response has already been committed
		if (isCommitted())
			throw new IllegalStateException();
		else {
			header_buff = new PrintWriter(m_output);
			respHeaders = new HashMap<String,StringBuffer>();
		}
	}

	@Override //DONE
	public void resetBuffer() {
//		Clears the content of the underlying buffer in the response without clearing headers or status code. If the response has been 
//		committed, this method throws an IllegalStateException. 
		if (isCommitted())
			throw new IllegalStateException();
		else {
			m_writer = new PrintWriter(new OutputStreamWriter(m_output));
		}
	}
	
	@Override //DONE
	public int getBufferSize() {
//	    Returns the actual buffer size used for the response. If no buffering is used, this method returns 0.
//
//	    Returns:
//	        the actual buffer size used
		return buffer_size;
	}

	@Override //DONE
	public void setBufferSize(int arg0) {
//	    Sets the preferred buffer size for the body of the response. The servlet container will use a buffer at least as large as 
//	    the size requested. The actual buffer size used can be found using getBufferSize.
//
//	    A larger buffer allows more content to be written before anything is actually sent, thus providing the servlet with more time 
//	    to set appropriate status codes and headers. A smaller buffer decreases server memory load and allows the client to start 
//	    receiving data more quickly.
//
//	    This method must be called before any response body content is written; if content has been written or the response object has 
//	    been committed, this method throws an IllegalStateException.
//
//	    Parameters:
//	        size - the preferred buffer size 
//	    Throws:
//	        java.lang.IllegalStateException - if this method is called after content has been written
		if (isCommitted())
			throw new IllegalStateException();
		else
			buffer_size = arg0;
	}

	@Override //DONE
	public void setCharacterEncoding(String arg0) {
//	    Sets the character encoding (MIME charset) of the response being sent to the client, for example, to UTF-8. If the character 
//	    encoding has already been set by setContentType(java.lang.String) or setLocale(java.util.Locale), this method overrides it. 
//	    Calling setContentType(java.lang.String) with the String of text/html and calling this method with the String of UTF-8 is 
//	    equivalent with calling setContentType with the String of text/html; charset=UTF-8.
//
//	    This method can be called repeatedly to change the character encoding. This method has no effect if it is called after getWriter 
//	    has been called or after the response has been committed.
//
//	    Containers must communicate the character encoding used for the servlet response's writer to the client if the protocol 
//	    provides a way for doing so. In the case of HTTP, the character encoding is communicated as part of the Content-Type header 
//	    for text media types. Note that the character encoding cannot be communicated via HTTP headers if the servlet does not specify 
//	    a content type; however, it is still used to encode text written via the servlet response's writer.
//
//	    Parameters:
//	        charset - a String specifying only the character set defined by IANA Character Sets 
//	        (http://www.iana.org/assignments/character-sets)
		if (!isCommitted()) {
			char_encoding = arg0;
			setHeader("Character-Encoding", char_encoding);
		}
	}

	@Override //DONE
	public void setContentLength(int arg0) {
//	    Sets the length of the content body in the response In HTTP servlets, this method sets the HTTP Content-Length header.
//
//	    Parameters:
//	        len - an integer specifying the length of the content being returned to the client; sets the Content-Length header
		if (!isCommitted()) {
			content_length = arg0;
			setIntHeader("Content-Length", content_length);
		}
	}

	@Override //DONE
	public void setContentType(String arg0) {
//	    Sets the content type of the response being sent to the client, if the response has not been committed yet. The given content 
//	    type may include a character encoding specification, for example, text/html;charset=UTF-8. The response's character encoding is 
//	    only set from the given content type if this method is called before getWriter is called.
//
//	    This method may be called repeatedly to change content type and character encoding. This method has no effect if called after 
//	    the response has been committed. It does not set the response's character encoding if it is called after getWriter has been 
//	    called or after the response has been committed.
//
//	    Containers must communicate the content type and the character encoding used for the servlet response's writer to the client 
//	    if the protocol provides a way for doing so. In the case of HTTP, the Content-Type header is used.
//
//	    Parameters:
//	        type - a String specifying the MIME type of the content
		if (!isCommitted()) {
			content_type = arg0;
			setHeader("Content-Type", content_type);
		}
	}

	@Override //DONE
	public void setLocale(Locale arg0) {
//	    Sets the locale of the response, if the response has not been committed yet. It also sets the response's character encoding 
//	    appropriately for the locale, if the character encoding has not been explicitly set using setContentType(java.lang.String) or 
//	    setCharacterEncoding(java.lang.String), getWriter hasn't been called yet, and the response hasn't been committed yet. If the 
//	    deployment descriptor contains a locale-encoding-mapping-list element, and that element provides a mapping for the given locale, 
//	    that mapping is used. Otherwise, the mapping from locale to character encoding is container dependent.
//
//	    This method may be called repeatedly to change locale and character encoding. The method has no effect if called after the 
//	    response has been committed. It does not set the response's character encoding if it is called after 
//	    setContentType(java.lang.String) has been called with a charset specification, after setCharacterEncoding(java.lang.String) 
//	    has been called, after getWriter has been called, or after the response has been committed.
//
//	    Containers must communicate the locale and the character encoding used for the servlet response's writer to the client if the 
//	    protocol provides a way for doing so. In the case of HTTP, the locale is communicated via the Content-Language header, the 
//	    character encoding as part of the Content-Type header for text media types. Note that the character encoding cannot be 
//	    communicated via HTTP headers if the servlet does not specify a content type; however, it is still used to encode text 
//	    written via the servlet response's writer.
//
//	    Parameters:
//	        loc - the locale of the response
		if (!isCommitted()) {
			theLocale = arg0;
			setHeader("Locale", theLocale.toString());
		}
	}
	
	@Override //DONE
	public ServletOutputStream getOutputStream() throws IOException {
		return null;
	}
	
	@Override //DONE
	public String encodeRedirectUrl(String arg0) {
		// Deprecated. As of version 2.1, use encodeRedirectURL(String url) instead
		return null;
	}
	
	@Override //DONE
	public String encodeUrl(String arg0) {
		// Deprecated. As of version 2.1, use encodeURL(String url) instead
		return null;
	}
	
	@Override //DONE
	public void setStatus(int arg0, String arg1) {
		// Deprecated. As of version 2.1, due to ambiguous meaning of the message parameter.
	}

}
